You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
749 lines
19 KiB
749 lines
19 KiB
import { Ionicons } from '@expo/vector-icons';
|
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
Alert,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { useConversation } from '../../src/hooks';
|
|
import { colors } from '../../src/theme';
|
|
import type { ChatCard, ChatMessage } from '../../src/types';
|
|
|
|
/* ===================== 子组件:顶部导航 ===================== */
|
|
|
|
function Header({
|
|
contactName,
|
|
companyName,
|
|
positionName,
|
|
avatar,
|
|
online,
|
|
onBack,
|
|
}: {
|
|
contactName: string;
|
|
companyName: string;
|
|
positionName: string;
|
|
avatar: string;
|
|
online: boolean;
|
|
onBack: () => void;
|
|
}) {
|
|
return (
|
|
<View style={styles.header}>
|
|
<Pressable style={styles.headerBtn} onPress={onBack} hitSlop={8}>
|
|
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
|
|
</Pressable>
|
|
|
|
<View style={styles.headerCenter}>
|
|
<View style={[styles.avatar, { backgroundColor: colors.primary }]}>
|
|
<Text style={styles.avatarText}>{avatar}</Text>
|
|
</View>
|
|
<View style={styles.headerInfo}>
|
|
<View style={styles.headerNameRow}>
|
|
<Text style={styles.headerName}>{contactName}</Text>
|
|
{online && <View style={styles.onlineDot} />}
|
|
</View>
|
|
<Text style={styles.headerSub}>
|
|
{companyName} · {positionName}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 原先这里是「更多」按钮,但项目没有单聊菜单页,点了只能弹「功能开发中」。
|
|
按「不留假交互」的口径摘掉,保留占位以维持标题居中(原先为空函数,点了无反应) */}
|
|
<View style={styles.headerBtn} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:双向匹配横幅 ===================== */
|
|
|
|
function MatchBanner({ score, contactName }: { score: number; contactName: string }) {
|
|
return (
|
|
<View style={styles.banner}>
|
|
<View style={styles.bannerIconWrap}>
|
|
<Ionicons name="heart" size={18} color={colors.success} />
|
|
</View>
|
|
<View style={styles.bannerBody}>
|
|
<Text style={styles.bannerTitle}>
|
|
恭喜!您与{contactName}双向匹配成功
|
|
</Text>
|
|
<Text style={styles.bannerMeta}>
|
|
匹配度 {score}% · 立即沟通
|
|
</Text>
|
|
</View>
|
|
<Ionicons name="chevron-forward" size={18} color={colors.successText} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:消息气泡 ===================== */
|
|
|
|
function Avatar({ text }: { text: string }) {
|
|
return (
|
|
<View style={[styles.avatar, { backgroundColor: colors.primary }]}>
|
|
<Text style={styles.avatarText}>{text}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function TextBubble({
|
|
message,
|
|
isUser,
|
|
}: {
|
|
message: ChatMessage;
|
|
isUser: boolean;
|
|
}) {
|
|
return (
|
|
<View
|
|
style={[
|
|
styles.bubble,
|
|
isUser ? styles.bubbleUser : styles.bubbleHr,
|
|
]}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.bubbleText,
|
|
isUser ? styles.bubbleTextUser : styles.bubbleTextHr,
|
|
]}
|
|
>
|
|
{message.content}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function SystemMessage({ content }: { content: string }) {
|
|
return (
|
|
<View style={styles.systemWrap}>
|
|
<Text style={styles.systemText}>{content}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:富消息卡片 ===================== */
|
|
|
|
function InterviewCard({ card }: { card: ChatCard }) {
|
|
return (
|
|
<View style={styles.cardBase}>
|
|
<View style={styles.cardHeader}>
|
|
<Ionicons name="clipboard-outline" size={16} color={colors.success} />
|
|
<Text style={styles.cardTitle}>{card.title}</Text>
|
|
</View>
|
|
<View style={styles.cardList}>
|
|
{card.items?.map((item, idx) => (
|
|
<View key={idx} style={styles.cardListRow}>
|
|
<View style={styles.cardBullet}>
|
|
<Text style={styles.cardBulletText}>{idx + 1}</Text>
|
|
</View>
|
|
<Text style={styles.cardItemText}>{item}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
{/* 原先这里挂着「查看全部 12 道预测题」按钮,但项目没有面试题库页。
|
|
卡片已经把 mock 给出的题目列全了,故摘掉按钮,不留「点了没反应」的入口 */}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function SalaryCard({ card }: { card: ChatCard }) {
|
|
return (
|
|
<View style={[styles.cardBase, styles.cardSalary]}>
|
|
<View style={styles.cardHeader}>
|
|
<Ionicons name="cash-outline" size={16} color={colors.info} />
|
|
<Text style={styles.cardTitle}>{card.title}</Text>
|
|
</View>
|
|
{card.subtitle && (
|
|
<Text style={styles.cardSubtitle}>{card.subtitle}</Text>
|
|
)}
|
|
{/* 原先挂着「查看详细薪资报告」按钮,但项目没有该岗位的详细薪资报告页
|
|
(`/tools/salary-calculator` 是通用计算器,不是本岗位报告)。摘掉按钮 */}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function OfferCard({ card }: { card: ChatCard }) {
|
|
const router = useRouter();
|
|
return (
|
|
<View style={[styles.cardBase, styles.cardOffer]}>
|
|
<View style={styles.cardOfferHeader}>
|
|
<Ionicons name="gift" size={20} color={colors.action} />
|
|
<View style={styles.cardOfferTitleWrap}>
|
|
<Text style={styles.cardOfferTitle}>{card.title}</Text>
|
|
{card.subtitle && <Text style={styles.cardOfferSubtitle}>{card.subtitle}</Text>}
|
|
</View>
|
|
</View>
|
|
{/* 只有下发了按钮文案才渲染 —— 没有文案的按钮会显示成一条空白的可点区域 */}
|
|
{card.actionText ? (
|
|
<Pressable style={styles.cardOfferBtn} onPress={() => router.push('/jobs/offer-compare')}>
|
|
<Text style={styles.cardOfferBtnText}>{card.actionText}</Text>
|
|
<Ionicons name="chevron-forward" size={14} color={colors.actionText} />
|
|
</Pressable>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function MessageCard({ card }: { card: ChatCard }) {
|
|
switch (card.type) {
|
|
case 'interview':
|
|
return <InterviewCard card={card} />;
|
|
case 'salary':
|
|
return <SalaryCard card={card} />;
|
|
case 'offer':
|
|
return <OfferCard card={card} />;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/* ===================== 子组件:单条消息行 ===================== */
|
|
|
|
function MessageRow({
|
|
message,
|
|
avatar,
|
|
}: {
|
|
message: ChatMessage;
|
|
avatar: string;
|
|
}) {
|
|
const isUser = message.role === 'user';
|
|
const isSystem = message.role === 'system';
|
|
|
|
if (isSystem) {
|
|
return <SystemMessage content={message.content ?? ''} />;
|
|
}
|
|
|
|
return (
|
|
<View
|
|
style={[
|
|
styles.messageRow,
|
|
isUser ? styles.messageRowRight : styles.messageRowLeft,
|
|
]}
|
|
>
|
|
{!isUser && <Avatar text={avatar} />}
|
|
<View style={styles.messageBody}>
|
|
{message.content ? (
|
|
<TextBubble message={message} isUser={isUser} />
|
|
) : null}
|
|
{message.card ? <MessageCard card={message.card} /> : null}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:时间戳 ===================== */
|
|
|
|
function Timestamp({ time }: { time: string }) {
|
|
return (
|
|
<View style={styles.timeWrap}>
|
|
<Text style={styles.timeText}>{time}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:底部输入框 ===================== */
|
|
|
|
function InputBar({
|
|
value,
|
|
onChange,
|
|
onSend,
|
|
sending,
|
|
}: {
|
|
value: string;
|
|
onChange: (text: string) => void;
|
|
onSend: () => void;
|
|
sending: boolean;
|
|
}) {
|
|
/*
|
|
* 表情 / 图片 / 文件 / 加号四个图标保留:输入区四图标是行业标准布局,删掉会让输入区显得残缺。
|
|
* 但它们依赖后端(附件上传、表情资源接口),本机给不了,故按项目 4.4 节「AI 按钮」的先例
|
|
* 保留入口 + 如实提示,不假装成功。
|
|
*
|
|
* 下方标记是本项目「知情保留的后端待办」记号:挡住审计脚本的误判,
|
|
* 又能在检索时把已知欠账数出来,不混进「有意为之」里消失。
|
|
* 后端就绪后 `node scripts/check-affordances.mjs --pending` 可一次列出全部欠账。
|
|
*/
|
|
const showPendingHint = () => {
|
|
// affordance-pending-backend
|
|
Alert.alert('暂未开放', '附件功能需后端支持,当前版本暂未开放');
|
|
};
|
|
|
|
return (
|
|
<View style={styles.inputBar}>
|
|
<View style={styles.inputLeftActions}>
|
|
<Pressable style={styles.inputIconBtn} onPress={showPendingHint}>
|
|
<Ionicons name="happy-outline" size={24} color={colors.textSecondary} />
|
|
</Pressable>
|
|
<Pressable style={styles.inputIconBtn} onPress={showPendingHint}>
|
|
<Ionicons name="image-outline" size={24} color={colors.textSecondary} />
|
|
</Pressable>
|
|
<Pressable style={styles.inputIconBtn} onPress={showPendingHint}>
|
|
<Ionicons name="folder-outline" size={24} color={colors.textSecondary} />
|
|
</Pressable>
|
|
</View>
|
|
|
|
<View style={styles.inputWrap}>
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="输入消息…"
|
|
placeholderTextColor={colors.textHint}
|
|
value={value}
|
|
onChangeText={onChange}
|
|
multiline
|
|
maxLength={500}
|
|
editable={!sending}
|
|
/>
|
|
<Pressable style={styles.plusBtn} onPress={showPendingHint}>
|
|
<Ionicons name="add" size={22} color={colors.textSecondary} />
|
|
</Pressable>
|
|
</View>
|
|
|
|
<Pressable
|
|
style={[styles.sendBtn, (!value.trim() || sending) && styles.sendBtnDisabled]}
|
|
onPress={onSend}
|
|
disabled={!value.trim() || sending}
|
|
>
|
|
<Ionicons name="send" size={18} color={colors.white} />
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 页面 ===================== */
|
|
|
|
export default function ChatDetailScreen() {
|
|
const router = useRouter();
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const { detail, loading, error, refresh, sendMessage, sending } = useConversation(id ?? '');
|
|
const [input, setInput] = useState('');
|
|
const scrollRef = useRef<ScrollView>(null);
|
|
|
|
const messages = useMemo(() => detail?.messages ?? [], [detail]);
|
|
|
|
const handleSend = useCallback(async () => {
|
|
const text = input.trim();
|
|
if (!text || sending) return;
|
|
setInput('');
|
|
await sendMessage(text);
|
|
// 滚动到底部
|
|
setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 100);
|
|
}, [input, sending, sendMessage]);
|
|
|
|
// 右上角「更多」入口已摘除:项目没有单聊菜单页,留着只能弹「功能开发中」
|
|
|
|
if (loading) {
|
|
return (
|
|
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
|
<View style={styles.stateBox}>
|
|
<ActivityIndicator size="large" color={colors.primary} />
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
if (error || !detail) {
|
|
return (
|
|
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
|
<View style={styles.stateBox}>
|
|
<Text style={styles.stateText}>{error ? '加载失败' : '会话不存在'}</Text>
|
|
<Pressable style={styles.retryBtn} onPress={refresh}>
|
|
<Text style={styles.retryText}>重试</Text>
|
|
</Pressable>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
|
<Header
|
|
contactName={detail.contactName}
|
|
companyName={detail.companyName}
|
|
positionName={detail.positionName}
|
|
avatar={detail.avatar}
|
|
online={detail.online}
|
|
onBack={() => router.back()}
|
|
/>
|
|
|
|
{detail.matchInfo && (
|
|
<MatchBanner score={detail.matchInfo.score} contactName={detail.contactName} />
|
|
)}
|
|
|
|
<KeyboardAvoidingView
|
|
style={styles.flex}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 100 : 0}
|
|
>
|
|
<ScrollView
|
|
ref={scrollRef}
|
|
style={styles.scroll}
|
|
contentContainerStyle={styles.scrollContent}
|
|
showsVerticalScrollIndicator={false}
|
|
onContentSizeChange={() => scrollRef.current?.scrollToEnd({ animated: false })}
|
|
>
|
|
{messages.map((message, index) => {
|
|
const showTime =
|
|
index === 0 ||
|
|
messages[index - 1].timestamp !== message.timestamp ||
|
|
message.role !== messages[index - 1].role;
|
|
return (
|
|
<View key={message.id}>
|
|
{showTime && message.timestamp ? (
|
|
<Timestamp time={message.timestamp} />
|
|
) : null}
|
|
<MessageRow message={message} avatar={detail.avatar} />
|
|
</View>
|
|
);
|
|
})}
|
|
</ScrollView>
|
|
|
|
<InputBar
|
|
value={input}
|
|
onChange={setInput}
|
|
onSend={handleSend}
|
|
sending={sending}
|
|
/>
|
|
</KeyboardAvoidingView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
safeArea: {
|
|
flex: 1,
|
|
backgroundColor: colors.pageBg,
|
|
},
|
|
flex: {
|
|
flex: 1,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 8,
|
|
height: 52,
|
|
backgroundColor: colors.white,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.borderLight,
|
|
},
|
|
headerBtn: {
|
|
width: 40,
|
|
height: 40,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
headerCenter: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
},
|
|
headerInfo: {
|
|
marginLeft: 10,
|
|
alignItems: 'center',
|
|
},
|
|
headerNameRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
headerName: {
|
|
fontSize: 16,
|
|
fontWeight: '700',
|
|
color: colors.textPrimary,
|
|
},
|
|
onlineDot: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 4,
|
|
backgroundColor: colors.success,
|
|
marginLeft: 6,
|
|
},
|
|
headerSub: {
|
|
marginTop: 2,
|
|
fontSize: 12,
|
|
color: colors.textSecondary,
|
|
},
|
|
avatar: {
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 18,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
avatarText: {
|
|
fontSize: 14,
|
|
fontWeight: '700',
|
|
color: colors.white,
|
|
},
|
|
banner: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginHorizontal: 8,
|
|
marginTop: 8,
|
|
padding: 10,
|
|
borderRadius: 8,
|
|
backgroundColor: colors.successLight,
|
|
},
|
|
bannerIconWrap: {
|
|
width: 34,
|
|
height: 34,
|
|
borderRadius: 17,
|
|
backgroundColor: colors.white,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginRight: 10,
|
|
},
|
|
bannerBody: {
|
|
flex: 1,
|
|
},
|
|
bannerTitle: {
|
|
fontSize: 14,
|
|
fontWeight: '700',
|
|
color: colors.successText,
|
|
},
|
|
bannerMeta: {
|
|
marginTop: 2,
|
|
fontSize: 12,
|
|
color: colors.success,
|
|
},
|
|
scroll: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
padding: 8,
|
|
paddingBottom: 8,
|
|
},
|
|
systemWrap: {
|
|
alignItems: 'center',
|
|
marginVertical: 10,
|
|
},
|
|
systemText: {
|
|
fontSize: 12,
|
|
color: colors.textHint,
|
|
},
|
|
timeWrap: {
|
|
alignItems: 'center',
|
|
marginVertical: 6,
|
|
},
|
|
timeText: {
|
|
fontSize: 11,
|
|
color: colors.textHint,
|
|
},
|
|
messageRow: {
|
|
flexDirection: 'row',
|
|
marginBottom: 10,
|
|
maxWidth: '82%',
|
|
},
|
|
messageRowLeft: {
|
|
alignSelf: 'flex-start',
|
|
},
|
|
messageRowRight: {
|
|
alignSelf: 'flex-end',
|
|
flexDirection: 'row-reverse',
|
|
},
|
|
messageBody: {
|
|
marginLeft: 8,
|
|
marginRight: 8,
|
|
},
|
|
bubble: {
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 9,
|
|
borderRadius: 12,
|
|
maxWidth: '100%',
|
|
},
|
|
bubbleHr: {
|
|
backgroundColor: colors.white,
|
|
borderBottomLeftRadius: 4,
|
|
},
|
|
bubbleUser: {
|
|
backgroundColor: colors.primary,
|
|
borderBottomRightRadius: 4,
|
|
},
|
|
bubbleText: {
|
|
fontSize: 14,
|
|
lineHeight: 20,
|
|
},
|
|
bubbleTextHr: {
|
|
color: colors.textPrimary,
|
|
},
|
|
bubbleTextUser: {
|
|
color: colors.white,
|
|
},
|
|
cardBase: {
|
|
width: 260,
|
|
backgroundColor: colors.white,
|
|
borderRadius: 8,
|
|
padding: 12,
|
|
marginTop: 6,
|
|
},
|
|
cardHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: 8,
|
|
},
|
|
cardTitle: {
|
|
marginLeft: 6,
|
|
fontSize: 14,
|
|
fontWeight: '700',
|
|
color: colors.textPrimary,
|
|
},
|
|
cardSalary: {
|
|
backgroundColor: colors.primaryLight,
|
|
},
|
|
cardList: {
|
|
marginBottom: 8,
|
|
},
|
|
cardListRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
marginBottom: 6,
|
|
},
|
|
cardBullet: {
|
|
width: 16,
|
|
height: 16,
|
|
borderRadius: 8,
|
|
backgroundColor: colors.successLight,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginRight: 6,
|
|
marginTop: 2,
|
|
},
|
|
cardBulletText: {
|
|
fontSize: 10,
|
|
fontWeight: '700',
|
|
color: colors.success,
|
|
},
|
|
cardItemText: {
|
|
flex: 1,
|
|
fontSize: 13,
|
|
color: colors.textPrimary,
|
|
},
|
|
cardSubtitle: {
|
|
fontSize: 12,
|
|
lineHeight: 18,
|
|
color: colors.textSecondary,
|
|
marginBottom: 10,
|
|
},
|
|
cardOffer: {
|
|
width: 260,
|
|
backgroundColor: colors.actionLight,
|
|
borderRadius: 8,
|
|
padding: 12,
|
|
marginTop: 6,
|
|
},
|
|
cardOfferHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: 10,
|
|
},
|
|
cardOfferTitleWrap: {
|
|
flex: 1,
|
|
marginLeft: 10,
|
|
},
|
|
cardOfferTitle: {
|
|
fontSize: 14,
|
|
fontWeight: '700',
|
|
color: colors.actionText,
|
|
},
|
|
cardOfferSubtitle: {
|
|
marginTop: 2,
|
|
fontSize: 12,
|
|
color: colors.textSecondary,
|
|
},
|
|
cardOfferBtn: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingTop: 8,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.actionBorder,
|
|
},
|
|
cardOfferBtnText: {
|
|
flex: 1,
|
|
fontSize: 12,
|
|
color: colors.actionText,
|
|
fontWeight: '600',
|
|
},
|
|
inputBar: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 6,
|
|
paddingVertical: 8,
|
|
backgroundColor: colors.white,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
inputLeftActions: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
inputIconBtn: {
|
|
width: 34,
|
|
height: 34,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
inputWrap: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginHorizontal: 4,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 4,
|
|
borderRadius: 18,
|
|
backgroundColor: colors.pageBg,
|
|
},
|
|
input: {
|
|
flex: 1,
|
|
maxHeight: 80,
|
|
minHeight: 34,
|
|
fontSize: 14,
|
|
color: colors.textPrimary,
|
|
paddingRight: 8,
|
|
},
|
|
plusBtn: {
|
|
width: 28,
|
|
height: 28,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
sendBtn: {
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 18,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: colors.primaryBtn,
|
|
},
|
|
sendBtnDisabled: {
|
|
opacity: 0.5,
|
|
},
|
|
stateBox: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
stateText: {
|
|
marginTop: 12,
|
|
fontSize: 14,
|
|
color: colors.textSecondary,
|
|
},
|
|
retryBtn: {
|
|
marginTop: 16,
|
|
paddingHorizontal: 24,
|
|
paddingVertical: 8,
|
|
borderRadius: 18,
|
|
backgroundColor: colors.primaryBtn,
|
|
},
|
|
retryText: {
|
|
fontSize: 13,
|
|
fontWeight: '600',
|
|
color: colors.white,
|
|
},
|
|
});
|
|
|